// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Text;
using System.Reflection;
using CIP4.Common;
using CIP4.Item;
using CIP4.Simple;

namespace CIP4.DataProvider.File
{
    public class DataProvider : CIP4.Simple.DataProvider, IDataProvider
    {
        DirectoryInfo ProviderDirectory;

        bool CompressData;

        IDataProvider Cache { get; set; }

        Dictionary<string, string> Collections = new Dictionary<string, string>();

        #region IDataProvider Members

        public string ConnectionString { get; private set; }

        public DataProvider (CIP4ConnectionAttribute DataProviderConnection, IDataProvider cachingProvider, LoggerDelegate logger)
            : base (DataProviderConnection)
        {
            Cache = cachingProvider;
            if (!(DataProviderConnection is CIP4FileConnectionAttribute))
                LoggerWrapper(MajorEnum.Error, MinorEnum.UnexpectedConnectionType,
                    "{0} is not an acceptable connection type", new object[] { DataProviderConnection.ToString() }, logger);
            else
            {
                LoggerWrapper(MajorEnum.Trace, MinorEnum.InitializingProvider,
                    "Initalizing Provider: {0}", new object[] { DataProviderConnection }, logger);
                CIP4FileConnectionAttribute connection = (CIP4FileConnectionAttribute)DataProviderConnection;
                ConnectionString = connection.ToString();
                ProviderDirectory = new DirectoryInfo(connection.BaseDirectoryPath);
                CompressData = connection.CompressData;
                if (!ProviderDirectory.Exists)
                    throw new ArgumentException(string.Format("The directory {0} does not exist - the file DataProvider provider requires an existing directory", ProviderDirectory.FullName));
            }
        }

        public void DeleteCollection(CollectionType collection, LoggerDelegate logger)
        {
            using (new CollectionLockType(collection.Name))
            {
                if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                    "Started {0}", new object[] { MinorEnum.DeleteCollection }, logger))
                    return;

                string collectionPath = GetCollectionFolder(collection);
                DirectoryInfo directory = new DirectoryInfo(collectionPath);

                //int count = directory.GetFiles("*." + GetFileExtension()).Count();
                //LoggerWrapper(MajorEnum.Trace, MinorEnum.Generic, "Deleting {0} files from {1}", 
                //    new object[] { count, collectionPath }, logger);
                directory.Delete(true);
                Collections.Remove(collection.ExternalIDString());

                if (Cache != null)
                    Cache.DeleteCollection(collection, logger);
                
                LoggerWrapper(MajorEnum.Information, MinorEnum.DeleteCollection,
                    "Collection deleted {0}", new object[] { collection }, logger);
            }
        }

        public override IEnumerable<object> GetItems(CollectionType collection, IBooleanValue selectionExpression, CursorType cursor)
        {
            using (new CollectionLockType(collection.Name))
            {
                List<object> items = new List<object>();
                if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                    "Started {0}", new object[] { MinorEnum.GetItems }, cursor.Logger))
                    return items;

                try
                {
                    IDataProvider Cache = null;
                    string collectionPath = GetCollectionFolder(collection);
                    DirectoryInfo directory = new DirectoryInfo(collectionPath);
                    if (!directory.Exists)
                        return items;

                    ISerializerType serializer = cursor.GetSerializer(SerializerKind);

                    string lookForExternalId = GetExternalIdValueFromSelectionExpression(selectionExpression, serializer);

                    if (Cache != null && !string.IsNullOrEmpty(lookForExternalId))
                    {
                        foreach (object item in Cache.GetItems(collection, selectionExpression, cursor))
                        {
                            LoggerWrapper(MajorEnum.Trace, MinorEnum.CacheHit, "Cache hit: {0}", new object[] { items[0] }, cursor.Logger);
                            items.Add(item);
                            return items;
                        }
                    }


                    string externalIDKey = GetExternalIDKey(lookForExternalId);

                    if (!string.IsNullOrEmpty(lookForExternalId))
                    {
                        try
                        {
                            directory.Refresh();

                            foreach (FileInfo file in directory.GetFiles(externalIDKey))
                            {
                                object item = ReadObject(file, cursor, serializer);
                                if (CompareProperties(selectionExpression, item, serializer))
                                {
                                    items.Add(item);
                                    GetItemsStreamResolver(serializer, collection, serializer.GetExternalID(item).ToString());
                                }
                                if (Cache != null)
                                    Cache.SaveItems(items, collection, cursor.GetSerializer);
                            }
                            return items;
                        }
                        catch (FileNotFoundException)
                        {
                            return items;
                        }
                    }

                    if (cursor.FailOnLinearSearch)
                        if (!LoggerWrapper(MajorEnum.Error, MinorEnum.FailOnLinearSearch,
                            "Failed on linear search accessing {0}", new object[] { collection }, cursor.Logger))
                            return items;

                    // Loop is kind of odd - need the marker to refer to the first item that will be returned on the next page
                    // this is to allow for deletes where the last item returned is deleted - if this was used as the marker
                    // nothing would be returned for the next page as the marker file would no longer be there. 
                    {
                        int i = 0;

                        if (cursor.Marker == null)
                            return items;

                        bool startProcessing = cursor.Marker == string.Empty;

                        string newMarker = null;

                        directory.Refresh();

                        foreach (FileInfo file in directory.GetFiles("*." + GetFileExtension()))
                        {
                            startProcessing = startProcessing || ExternalIDFromFile(file) == cursor.Marker;
                            if (startProcessing)
                            {
                                object item = ReadObject(file, cursor, serializer);
                                if (CompareProperties(selectionExpression, item, serializer))
                                {
                                    if (i >= cursor.PageSize)
                                    {
                                        newMarker = serializer.GetExternalID(item).ToString();
                                        break;
                                    }
                                    items.Add(item);
                                    GetItemsStreamResolver(serializer, collection, serializer.GetExternalID(item).ToString());
                                    i += 1;
                                }
                            }
                        }
                        cursor.Marker = newMarker;
                        if (Cache != null)
                            Cache.SaveItems(items, collection, cursor.GetSerializer);
                        return items;
                    }
                }
                finally
                {
                    LoggerWrapper(MajorEnum.Information, MinorEnum.GetItems,
                        "Get {0} items from {1}", new object[] { items.Count, collection }, cursor.Logger);
                }
            }
        }

        public override void PurgeItems(IEnumerable<ExternalIDType> externalIDs, CollectionType collection, GetSerializerDelegate getSerializer)
        {
            using (new CollectionLockType(collection.Name))
            {
                ISerializerType serializer = getSerializer(SerializerKind);

                if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                    "Started {0}", new object[] { MinorEnum.PurgeItems }, serializer.Logger))
                    return;

                string collectionPath = GetCollectionFolder(collection);
                int deletedCount = 0;
                foreach (ExternalIDType externalID in externalIDs)
                {
                    if (System.IO.File.Exists(collectionPath + "\\" + externalID.ToString() + "." + GetFileExtension()))
                    {
                        System.IO.File.Delete(collectionPath + "\\" + externalID.ToString() + "." + GetFileExtension());

                        RemoveItemsStreamResolver(serializer, collection, externalID.ToString());

                        LoggerWrapper(MajorEnum.Trace, MinorEnum.ItemDeleted, "deleted item {0}", new object[] { externalID }, serializer.Logger);

                        deletedCount += 1;
                    }
                }

                if (Cache != null)
                    Cache.PurgeItems(externalIDs, collection, getSerializer);

                if (deletedCount == 0)
                    LoggerWrapper(MajorEnum.Warning, MinorEnum.NothingDeleted, "Nothing deleted from {0}", new object[] { collection.Name }, serializer.Logger);

                LoggerWrapper(MajorEnum.Information, MinorEnum.PurgeItems,
                    "{0} items deleted from {1}", new object[] { deletedCount, collection.Name }, serializer.Logger);
            }
        }

        public void SaveItems(IEnumerable<object> items, CollectionType collection, GetSerializerDelegate getSerializer)
        {
            using (new CollectionLockType(collection.Name))
            {
                ISerializerType serializer = getSerializer(SerializerKind);

                if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                    "Started {0}", new object[] { MinorEnum.SaveItems }, serializer.Logger))
                    return;

                string collectionPath = GetCollectionFolder(collection);
                int itemCount = 0;
                foreach (object itemObject in items)
                {
                    if (!SetHeaderProperties(itemObject, collection, serializer))
                        continue;
                    if (serializer.CheckVersion)
                    {
                        FileInfo fi = new FileInfo(collectionPath + "\\" + serializer.GetExternalID(itemObject) + "." + GetFileExtension());
                        if (fi.Exists && fi.LastWriteTimeUtc.Ticks > serializer.GetVersionNumber(itemObject))
                            if (serializer.Logger(MajorEnum.Error, MinorEnum.StaleItemOnSave, "The Item {0} has been updated by another user", new object[] { serializer.GetExternalID(itemObject).ToString() }) == LoggerResponseEnum.giveUp)
                                return;
                    }
                    int retryCount = 0;
                    do
                    {
                        FileStream fs = null;
                        BinaryWriter bw = null;
                        StreamWriter wrtr = null;
                        try
                        {
                            if (CompressData)
                            {
                                fs = new FileStream(collectionPath + "\\" + serializer.GetExternalID(itemObject) + ".data", FileMode.Create, FileAccess.ReadWrite);
                                bw = new BinaryWriter(fs);
                                bw.Write(Serializer.SerializerBaseType.Compress(serializer.GetDataForObject(itemObject).ToString()));
                            }
                            else
                            {
                                wrtr = new StreamWriter(collectionPath + "\\" + serializer.GetExternalID(itemObject) + "." + GetFileExtension());
                                wrtr.Write(serializer.GetDataForObject(itemObject));
                                wrtr.Close();
                            }
                            SaveItemsStreamResolver(serializer, collection, serializer.GetExternalID(itemObject).ToString());

                            LoggerWrapper(MajorEnum.Trace, MinorEnum.ItemsUpdated, "Items {0} updated", new object[] { serializer.GetExternalID(itemObject) }, serializer.Logger);
                            itemCount++;
                            break;
                        }
                        catch (System.IO.IOException)
                        {
                            retryCount += 1;
                            if (retryCount == 4)
                            {
                                if (LoggerWrapper(MajorEnum.Error, MinorEnum.ErrorSavingItems, "IOException handling {0}, retryCount={1}", new object[] { serializer.GetExternalID(itemObject) }, serializer.Logger))
                                    break;
                                else
                                    return;
                            }
                            System.Threading.Thread.Sleep(retryCount * 1000);
                        }
                        finally
                        {
                            if (bw != null)
                                bw.Close();
                            if (wrtr != null)
                                wrtr.Close();
                        }
                    } while (true);
                }

                if (Cache != null)
                    Cache.SaveItems(items, collection, getSerializer);

                LoggerWrapper(MajorEnum.Information, MinorEnum.SaveItems,
                    "{0} items saved in {1}", new object[] { itemCount, collection.Name }, serializer.Logger);
            }
        }

        public void Terminate(LoggerDelegate logger)
        {
        }

        #endregion

        #region Helper Functions

        private string GetFileExtension()
        {
            return CompressData ? "data" : 
                SerializerKind == SerializerKindEnum.CIP4Json? "json" : "xml";
        }

        private object ReadObject(FileInfo file, CursorType cursor, ISerializerType serializer)
        {
            FileStream bs = null;
            StreamReader rdr = null;
            try
            {
                if (CompressData)
                {
                    bs = new FileStream(file.FullName, FileMode.Open);
                    byte[] data = new byte[bs.Length];
                    int offset = 0;
                    int remaining = data.Length;
                    while (remaining > 0)
                    {
                        int read = bs.Read(data, offset, remaining);
                        if (read <= 0)
                            throw new EndOfStreamException
                                (String.Format("End of stream reached with {0} bytes left to read", remaining));
                        remaining -= read;
                        offset += read;
                    }
                    object item = serializer.GetObjectForData(XElement.Parse(Serializer.SerializerBaseType.DeCompress(data)));
                    serializer.SetVersionNumber(item, file.LastWriteTimeUtc.Ticks);
                    return item;
                }
                else
                {
                    rdr = new StreamReader(file.FullName);
                    object item = serializer.GetObjectForData(rdr.ReadToEnd());
                    serializer.SetVersionNumber(item, file.LastWriteTimeUtc.Ticks);
                    return item;
                }
            }
            finally
            {
                if (bs != null)
                    bs.Close();
                if (rdr != null)
                    rdr.Close();
            }
        }

        private string GetExternalIDKey(string externalID)
        {
            if (string.IsNullOrEmpty(externalID))
                return string.Empty;

            return externalID + "." + GetFileExtension();
        }

        private void RemoveItemsStreamResolver(ISerializerType serializer, CollectionType collection, string externalID)
        {
            string collectionFolder = GetCollectionFolder(collection);
            foreach (KeyValuePair<string, KeyValuePair<PropertyInfo, object>> streamSeed in serializer.StreamSeeds)
            {
                string qualifiedName = Path.Combine(collectionFolder, externalID + "." + streamSeed.Key);
                System.IO.File.Delete(qualifiedName);
            }
        }

        private void GetItemsStreamResolver(ISerializerType serializer, CollectionType collection, string externalID)
        {
            string collectionFolder = GetCollectionFolder(collection);
            foreach (KeyValuePair<string, KeyValuePair<PropertyInfo, object>> streamSeed in serializer.StreamSeeds)
            {
                if (streamSeed.Value.Value == null)
                    continue;
                string qualifiedName = Path.Combine(collectionFolder, externalID + "." + streamSeed.Key);
                Stream str = new StreamReader(qualifiedName).BaseStream;
                streamSeed.Value.Key.SetValue(streamSeed.Value.Value, str, new object[] { });
            }
        }

        private void SaveItemsStreamResolver(ISerializerType serializer, CollectionType collection, string externalID)
        {
            string collectionFolder = GetCollectionFolder(collection);
            foreach (KeyValuePair<string, KeyValuePair<PropertyInfo, object>> streamSeed in serializer.StreamSeeds)
            {
                if (streamSeed.Value.Value == null)
                    continue;
                string qualifiedName = Path.Combine(collectionFolder, externalID + "." + streamSeed.Key);
                using (Stream str = new StreamWriter(qualifiedName).BaseStream)
                {
                    Stream source = (Stream)streamSeed.Value.Value;
                    if (source != null)
                    {
                        byte[] buffer = new byte[2048]; // pick size
                        int bytesRead;
                        while ((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0)
                        {
                            str.Write(buffer, 0, bytesRead);
                        }
                        source.Close();
                        str.Close();
                    }
                    source.Close();
                    source.Dispose();
                }
            }
        }

        private string ExternalIDFromFile(FileInfo file)
        {
            return Path.GetFileNameWithoutExtension(file.Name);
        }

        private string GetCollectionFolder(CollectionType collection)
        {
            if (!Collections.ContainsKey(collection.ExternalIDString()))
            {
                if (!Directory.Exists(ProviderDirectory.FullName + "\\" + collection.Name))
                    Directory.CreateDirectory(ProviderDirectory.FullName + "\\" + collection.Name);
                Collections.Add(collection.ExternalIDString(), ProviderDirectory.FullName + "\\" + collection.Name);
            }
            return Collections[collection.ExternalIDString()];
        }

        #endregion

    }
}
